iT邦幫忙

2023 iThome 鐵人賽

DAY 26
0
Modern Web

由前向後,從前端邁向全端系列 第 26

26.【從前端到全端,Nextjs+Nestjs】加入authenication(一)

  • 分享至 

  • xImage
  •  

文章重點

  • 創建User API

本文

今天我們來加入authenication。

我們先安裝使用的套件,我們使用的Authenication Library為passport,並且我們使用JWT strategy作為我們認證方法

pnpm add @nestjs/passport @nestjs/jwt passport passport-jwt bcrypt graphql-scalars class-validator

在開始創建auth前,我們先創建User API

首先,先對我們的graphql schema進行修改

scalar DateTime
scalar JWT

input NewProductInput {
	name: String!
	price: Float!
	description: String!
	imageUrl: String!
}

input UpdateProductInput {
	id: ID!
	name: String!
	price: Float!
	description: String!
	imageUrl: String!
}

type Product {
	id: ID!
	name: String!
	price: Float!
	description: String!
	imageUrl: String!
}

type ProductEdge {
	cursor: String!
	node: Product!
}

type ProductConnection {
	edges: [ProductEdge!]!
	nodes: [Product!]!
	pageInfo: PageInfo!
	totalCount: Int!
}

type PageInfo {
	endCursor: String
	hasNextPage: Boolean!
	hasPreviousPage: Boolean!
	startCursor: String
}

type CartItem {
	productId: ID!
	productName: String!
	price: Float!
	quantity: Int!
}

input CartItemInput {
	productId: ID!
	quantity: Int!
}

type Order {
	id: ID!
	items: [CartItem!]!
	orderDate: DateTime!
}

type Auth {
	accessToken: JWT!
	refreshToken: JWT!
	user: User!
}

input LoginInput {
	email: String!
	password: String!
}

input SignupInput {
	email: String!
	password: String!
	username: String!
}

input RefreshTokenInput {
	token: JWT!
}

input UpdateUserDataInput {
	username: String
}

enum Role {
	ADMIN
	CREATOR
	USER
}

type User {
	createdAt: DateTime!
	email: String!
	username: String!
	id: ID!
	role: Role!
	updatedAt: DateTime!
}

type Token {
	accessToken: JWT!
	refreshToken: JWT!
}

type Query {
	me(userId: ID!): User!
	getProducts(first: Int, after: String, last: Int, before: String): ProductConnection!
	getProduct(id: ID!): Product
	getUserProfile: User
	getCartItems: [CartItem!]!
}

type Mutation {
	login(data: LoginInput!): Auth!
	signup(data: SignupInput!): Auth!
	refreshToken(token: RefreshTokenInput!): Token!
	updateUser(userId: ID!, newUserData: UpdateUserDataInput!): User!
	addProduct(input: NewProductInput!): Product
	updateProduct(input: UpdateProductInput!): Product
	deleteProduct(id: ID!): Boolean
	addCartItem(productId: ID!, quantity: Int!): [CartItem!]!
	removeCartItem(productId: ID!): [CartItem!]!
	updateCartItem(productId: ID!, quantity: Int!): [CartItem!]!
	checkout(cartItems: [CartItemInput!]!): Order
}

然後執行pnpm exec nx run iron-ecommerce-server:gen-gql-type重新生成type。

並且修改我們的prisma schema,打開apps\iron-ecommerce-server\prisma\schema.prisma

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Product {
  id          String   @id @default(cuid())
  name        String
  price       Float
  description String
  imageUrl    String
}

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  username String
  password  String   // Encrypted password
  role      Role     @default(USER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

enum Role {
  ADMIN
  USER
}


執行pnpm exec nx run iron-ecommerce-server:prisma-generatenpm exec nx run iron-ecommerce-server:prisma-db-push,並且restart ts server

接下來,先在我們的config加入security

///// apps\iron-ecommerce-server\src\common\configs\config.interface.ts
export interface Config {
	nest: NestConfig;
	cors: CorsConfig;
	graphql: GraphqlConfig;
	security: SecurityConfig;
}

export interface NestConfig {
	port: number;
}

export interface CorsConfig {
	enabled: boolean;
}

export interface GraphqlConfig {
	codefirst: {
		playgroundEnabled: boolean;
		debug: boolean;
		schemaDestination: string;
		sortSchema: boolean;
	};
	schemafirst: {
		playgroundEnabled: boolean;
		typePaths: string[];
		definitions: {
			path: string;
		};
	};
}

export interface SecurityConfig {
	expiresIn: string;
	refreshIn: string;
	bcryptSaltOrRound: string | number;
}


///// apps\iron-ecommerce-server\src\common\configs\config.interface.ts

export interface Config {
	nest: NestConfig;
	cors: CorsConfig;
	graphql: GraphqlConfig;
	security: SecurityConfig;
}

// ....

export interface SecurityConfig {
	expiresIn: string;
	refreshIn: string;
	bcryptSaltOrRound: string | number;
}

接著我們開始實現我們的User API

///// apps\iron-ecommerce-server\src\api\auth\password.service.ts
import { SecurityConfig } from "../../common/configs/config.interface";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";

import { compare, hash } from "bcrypt";

@Injectable()
export class PasswordService {
	get bcryptSaltRounds(): string | number {
		const securityConfig = this.configService.get<SecurityConfig>("security");
		const saltOrRounds = securityConfig.bcryptSaltOrRound;

		return Number.isInteger(Number(saltOrRounds)) ? Number(saltOrRounds) : saltOrRounds;
	}

	constructor(private configService: ConfigService) {}

	validatePassword(password: string, hashedPassword: string): Promise<boolean> {
		return compare(password, hashedPassword);
	}

	hashPassword(password: string): Promise<string> {
		return hash(password, this.bcryptSaltRounds);
	}
}

/// USER
///// apps\iron-ecommerce-server\src\api\users\users.model.ts
import { Field, HideField, ID, ObjectType, registerEnumType } from "@nestjs/graphql";
import "@prisma/client";
import { Role } from "@prisma/client";

import { IsEmail } from "class-validator";

registerEnumType(Role, {
	name: "Role",
	description: "User role"
});

@ObjectType()
export class User {
	@Field(() => ID)
	id: string;

	@Field()
	@IsEmail()
	email: string;

	@Field(() => String, { nullable: true })
	firstname?: string;

	@Field(() => String, { nullable: true })
	lastname?: string;

	@Field(() => Role)
	role: Role;

	@HideField()
	password: string;

	@Field({
		description: "Identifies the date and time when the object was created."
	})
	createdAt: Date;

	@Field({
		description: "Identifies the date and time when the object was last updated."
	})
	updatedAt: Date;
}


///// apps\iron-ecommerce-server\src\api\users\users.service.ts
import { Injectable } from "@nestjs/common";

import { PrismaService } from "nestjs-prisma";

@Injectable()
export class UsersService {
	constructor(private prisma: PrismaService) {}

	async findUserById(userId: string) {
		return this.prisma.user.findUnique({
			where: { id: userId }
		});
	}

	async updateUser(userId: string, newUserData: { firstname?: string; lastname?: string }) {
		return this.prisma.user.update({
			data: {
				...newUserData,
				updatedAt: new Date()
			},
			where: {
				id: userId
			}
		});
	}
}


///// apps\iron-ecommerce-server\src\api\users\users.resolver.ts
import { User } from "./users.model";
import { UsersService } from "./users.service";
import { Args, Mutation, Query, Resolver } from "@nestjs/graphql";

@Resolver(() => User)
export class UsersResolver {
	constructor(private usersService: UsersService) {}

	@Query(() => User)
	async me(@Args("userId") userId: string): Promise<User> {
		return this.usersService.findUserById(userId);
	}

	@Mutation(() => User)
	async updateUser(
		@Args("userId") userId: string,
		@Args("newUserData") newUserData: { firstname?: string; lastname?: string }
	) {
		return this.usersService.updateUser(userId, newUserData);
	}
}

///// apps\iron-ecommerce-server\src\api\users\users.module.ts
import { PasswordService } from "../auth/password.service";
import { UsersResolver } from "./users.resolver";
import { UsersService } from "./users.service";
import { Module } from "@nestjs/common";

@Module({
	imports: [],
	providers: [UsersResolver, UsersService, PasswordService]
})
export class UsersModule {}


並且將module加入到我們的app module

///// apps\iron-ecommerce-server\src\app\app.module.ts
import { ProductsModule } from "../api/products/products.module";
import { UsersModule } from "../api/users/users.module";
import config from "../common/configs/config";
import { GraphQLSetupModule } from "../graphql/graphql-setup.module";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";

@Module({
	imports: [ConfigModule.forRoot({ isGlobal: true, load: [config] }), GraphQLSetupModule, ProductsModule, UsersModule],
	controllers: [AppController],
	providers: [AppService]
})
export class AppModule {}


總結

本篇已經創建了user相關的API,下一篇會繼續創建Auth相關的API


上一篇
25.【從前端到全端,Nextjs+Nestjs】在Nestjs GraphQL加入Pagination
下一篇
27.【從前端到全端,Nextjs+Nestjs】加入authenication (二)
系列文
由前向後,從前端邁向全端30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言